iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
AI Engineering

從行程到應援:30 天做出追星實用工具箱系列 第 26 篇

Day 26:實作「自訂專屬字典」管理介面

  • 分享至 

  • xImage
  •  

一、前言

因為目前的字典是寫死的,但追星時會有不同團體不同專有名詞、成員暱稱與流行語,想做個讓使用者可以自己新增「團體名、成員暱稱、粉絲名」的地方,讓翻譯起來可以更順暢

二、今日任務

1.建立字典資料模型

在 lib/features/live_translate/data/models/ 建立 fandom_word.dart

class FandomWord {
  final String id;
  final String original;  // 原詞
  final String translated; // 中文翻譯/中文暱稱
  final String category;   // 分類(如:成員暱稱, 團體名, 飯圈梗)

  FandomWord({
    required this.id,
    required this.original,
    required this.translated,
    this.category = '成員暱稱',
  });

  Map<String, dynamic> toJson() => {
        'id': id,
        'original': original,
        'translated': translated,
        'category': category,
      };

  factory FandomWord.fromJson(Map<String, dynamic> json) => FandomWord(
        id: json['id'],
        original: json['original'],
        translated: json['translated'],
        category: json['category'] ?? '成員暱稱',
      );
}

2.本地字典儲存服務

在 lib/features/live_translate/data/services/ 建立 dictionary_service.dart

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/fandom_word.dart';

class DictionaryService {
  static const String _key = 'user_fandom_dictionary';

  /// 取得所有自訂單字
  Future<List<FandomWord>> getWords() async {
    final prefs = await SharedPreferences.getInstance();
    final String? data = prefs.getString(_key);
    if (data == null || data.isEmpty) return _getDefaultInitialWords();

    final List<dynamic> jsonList = jsonDecode(data);
    return jsonList.map((e) => FandomWord.fromJson(e)).toList();
  }

  /// 新增單字
  Future<void> addWord(FandomWord word) async {
    final words = await getWords();
    words.add(word);
    await _saveWords(words);
  }

  /// 刪除單字
  Future<void> deleteWord(String id) async {
    final words = await getWords();
    words.removeWhere((item) => item.id == id);
    await _saveWords(words);
  }

  Future<void> _saveWords(List<FandomWord> words) async {
    final prefs = await SharedPreferences.getInstance();
    final String jsonString = jsonEncode(words.map((e) => e.toJson()).toList());
    await prefs.setString(_key, jsonString);
  }

  /// 預設基礎詞條
  List<FandomWord> _getDefaultInitialWords() {
    return [
      FandomWord(id: '1', original: '김지원', translated: '金志垣', category: '成員暱稱'),
      FandomWord(id: '2', original: '얼죽아', translated: '凍死也要喝冰美式', category: '飯圈梗'),
    ];
  }
}

3.打造字典管理介面

在 lib/features/live_translate/presentation/pages/ 建立 fandom_dictionary_page.dart

import 'package:flutter/material.dart';
import '../../data/models/fandom_word.dart';
import '../../data/services/dictionary_service.dart';

class FandomDictionaryPage extends StatefulWidget {
  const FandomDictionaryPage({Key? key}) : super(key: key);

  @override
  State<FandomDictionaryPage> createState() => _FandomDictionaryPageState();
}

class _FandomDictionaryPageState extends State<FandomDictionaryPage> {
  final DictionaryService _service = DictionaryService();
  List<FandomWord> _words = [];

  @override
  void initState() {
    super.initState();
    _loadDictionary();
  }

  Future<void> _loadDictionary() async {
    final list = await _service.getWords();
    setState(() => _words = list);
  }

  void _showAddDialog() {
    final origCtrl = TextEditingController();
    final transCtrl = TextEditingController();

    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('新增自訂專有名詞'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(controller: origCtrl, decoration: const InputDecoration(labelText: '原詞 (韓/日文)')),
            TextField(controller: transCtrl, decoration: const InputDecoration(labelText: '對應譯名/暱稱')),
          ],
        ),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
          ElevatedButton(
            onPressed: () async {
              if (origCtrl.text.isNotEmpty && transCtrl.text.isNotEmpty) {
                await _service.addWord(FandomWord(
                  id: DateTime.now().millisecondsSinceEpoch.toString(),
                  original: origCtrl.text.trim(),
                  translated: transCtrl.text.trim(),
                ));
                Navigator.pop(ctx);
                _loadDictionary();
              }
            },
            child: const Text('新增'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('自訂偶像專屬字典')),
      body: ListView.builder(
        itemCount: _words.length,
        itemBuilder: (ctx, i) {
          final word = _words[i];
          return ListTile(
            title: Text('${word.original} ➔ ${word.translated}'),
            subtitle: Text(word.category),
            trailing: IconButton(
              icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
              onPressed: () async {
                await _service.deleteWord(word.id);
                _loadDictionary();
              },
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _showAddDialog,
        child: const Icon(Icons.add),
      ),
    );
  }
}

上一篇
Day 25:例外處理與使用者體驗防呆機制
下一篇
Day 27:動態字典注入 Prompt 與同傳實測
系列文
從行程到應援:30 天做出追星實用工具箱 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言